iT邦幫忙

2026 iThome 鐵人賽

DAY 19
0
AI Security

Microsoft Foundry 的 AI Agent 攻防實戰:建立高資安與可治理的 Agent 系統系列 第 19

Day 19|Microsoft Foundry 的 AI Agent 攻防實戰:Secret 搬進 Key Vault,舊影印本為什麼還能用?

  • 分享至 

  • xImage
  •  

Day 18 已經分清誰能管理 vault、誰能讀 secret data。今天假設大魔術熊貓工程司仍有一個老舊供應商 API,無法使用 Managed Identity,只能提供 secret。把值搬進 Azure Key Vault 當然比寫死在 source code 好,但它只解決一部分問題。

如果 process 啟動時讀了一次 secret,放進全域變數,再活上三個月;或者 Agent adapter 把值塞進 trace 與 tool error,vault 裡的保護並不會跟著值一起移動。Secret 搬家了,舊影印本不會自動從抽屜、cache 和 log 裡消失。

先備名詞:Secret 的名稱、版本和值不可混在一起

  • Secret reference:只描述 vault、名稱與版本的指標,不包含真正的敏感值。
  • Version:同一個 secret 每次輪替後的特定版本;latest 是會移動的選擇,不是固定版本。
  • Resolve:adapter 在最後必要時刻,依已授權的 reference 取回值。
  • Zero-read deny:若政策拒絕或 binding 被竄改,secret store 的讀取次數必須是 0;「先讀再丟掉」仍然越過了資料邊界。

Threat:Vault 只管入口,process 可能繼續用舊值

輪替作業若只確認「Version 2 已建立」,會漏掉三個需要分開觀察的狀態:Version 1 是否 disabled、adapter 是否仍可選到它,以及 raw output 是否還留著舊值。今天的本機 fixture 就固定這三個狀態,不假設 Azure Key Vault 已經替應用清掉 cache。

Attack:Vault 已經輪替,Agent 還握著 Version 1

畫面判讀目標: 看見 vault 狀態已 disabled 時舊 adapter 仍選到 Version 1。

Secret adapter UI 顯示 disabled Version 1 被脆弱流程接受並產生一張 Receipt。

Vault 已輪替,不代表 process 裡的舊版本已失效。 可觀察狀態:selected_version=1、selected_enabled=false、reason=LEGACY_DISABLED_SECRET_ACCEPTED、Receipt=1。 Claim boundary:畫面不輸出 secret value,也不證明 proposal 取得 canary;未呼叫 Azure Key Vault。

真實使用者碰到的通常不是 version number,而是「供應商那邊剛換過密碼,怎麼還是連不上?幫我看看。」這句話不含 secret 名稱、vault URL、工具函式或預期結果;版本選擇是受信任 adapter 與部署設定的責任。

本機 Lab 不連 Azure,也不放任何真實 secret。測試框架另外準備兩個明顯的 canary,專門檢查舊版本是否仍被選到,以及值是否流進輸出 sink。它們不屬於 user message:

version 1 = SYNTH_MAGIC_PANDA_VENDOR_V1
version 2 = SYNTH_MAGIC_PANDA_VENDOR_V2

attack fixture 先建立 version 1,再 rotate 到 version 2。rotation 會把 version 1 標記為 disabled;before profile 仍容許 adapter 解析這個 disabled reference,因此留下:

{
  "action": "use_secret_version",
  "actor": "vault-agent",
  "allowed": true,
  "resource_id": "vendor-token:v1",
  "reason_code": "LEGACY_DISABLED_SECRET_ACCEPTED",
  "receipt_count": 1,
  "side_effect_count": 0,
  "details": {
    "selected_version": 1,
    "selected_enabled": false
  }
}

這筆 executor Receipt 只證明本機 adapter 邊界接受了 disabled version,沒有呼叫真實供應商,也不能宣稱對方仍接受舊 credential。不過它已經比「Key Vault 裡看到 Version 2」多回答一個重要問題:應用是否還能選到舊值。下游撤銷、cache 失效與舊值負向 smoke 仍要在雲端補證據。

同一個 canary 還會刻意進入五個 raw fixture:

  1. response:tool error 把值回給使用者。
  2. event:安全事件直接保存 exception payload。
  3. fake outbox:通知 body 帶到值。
  4. error:adapter diagnostic 拼入值。
  5. trace:span attribute 在 export 前收到值。

這裡沿用 Day 14 的 serializer。before scan 必須真的看到 canary,after-export scan 才要求五個 sink 全為 0;如果測試資料從頭就沒有 canary,零命中只是分母消失。

Key Vault 管的是值的入口,不是值離開後的人生

Azure Key Vault 的 control plane 與 data plane 獨立授權。Key Vault Contributor 用來管理 vault control plane,在 Azure RBAC permission model 下不能讀 secret value;只需讀值的 runtime 使用 Key Vault Secrets User,管理 secret 版本與生命週期的 operator 才需要 Key Vault Secrets OfficerKey Vault RBAC guide(查閱:2026-08-03)

工作 建議角色 不代表
建立/設定 vault 適當 control-plane role 可讀 secret value
runtime 讀既有 secret Key Vault Secrets User 可 set/delete/rotate
secret lifecycle operator Key Vault Secrets Officer 可管理 role assignment
Foundry BYO Key Vault connection resource MI 使用官方要求的最小 connection role 等同應用 runtime reader

legacy access-policy model 要另外小心。Microsoft 文件提醒,若 principal 擁有包含 Microsoft.KeyVault/vaults/write 的 Contributor 類 control-plane 權限,它可能替自己加入 access policy,進而取得 data-plane 權限。使用 Azure RBAC permission model 時,Key Vault Contributor 本身仍不能讀值;role assignment 又是另一項權限。

截至 2026-08-03,使用 Key Vault control-plane API version 2026-02-01 或更新版本建立 vault,若沒有明確指定,預設 access-control model 是 Azure RBAC;既有 vault 不會因為換 API version 更新就自動從 access policy 遷移。所有早於 2026-02-01 的 Key Vault control-plane API versions 預定於 2027-02-27 退役,data-plane API 不在這次退役範圍。Key Vault access-control default(查閱:2026-08-03)

2026-02-01 是 API version,不是某一天才開始生效的 feature flag;IaC 必須明確保存實際使用的 version 與 permission model。

Fix:先綁定 reference,再在 adapter 最後一刻解析

畫面判讀目標: 確認 proposal 只攜帶 approved reference,value 在 adapter 最後一刻解析。

Secret flow 顯示 proposal 只帶 reference,adapter 最後才解析值。

先授權 reference,再在最靠近使用點解析。 可觀察狀態:action hash 綁 reference/version policy,raw secret value 不出現在前段。 Claim boundary:不證明 Azure SDK、vault resource allowlist 或 network path。

今天不是把字串 kv://something 塞進 prompt 就算完成。SecretReference 使用
strict、extra="forbid"、frozen 的 Pydantic model,只接受 vault_aliasname
整數 version;data-plane scope 由 server 依這三個欄位派生。../finance-token
字串型 version,或偷偷多帶一個 value 欄位都會在入口失敗。

接著把同一個 reference 綁過四個階段:

SecretUseRequest
→ trusted planner 建立 SecretUseProposal
→ policy-owned SecretDecisionAuthority 獨立簽發 opaque decision
→ SecretPolicyDecision 綁定 action_hash、reference 與 provenance
→ adapter 先驗 decision provenance/hash/reference
→ 全部通過後才讀取指定 secret version

D19-A02 故意讓 request 指向 vendor token,proposal 卻換成另一個 reference。就算
兩個物件各自都符合 schema,也會得到 SECRET_PROPOSAL_BINDING_DENIED、Receipt=0。
另一個測試直接改寫 policy 的 action hash,adapter 回
SECRET_ADAPTER_ACTION_HASH_MISMATCH。這是防 TOCTOU 與 confused-deputy,不是
secret 名稱的字串黑名單。這些 deny path 以 spy store 斷言讀取次數為 0;decision 不是
adapter 看到 proposal 後自己現場捏出來的 allow。

user_message 只表達「確認供應商連線」這類業務需求;SecretReference 由受信任設定與 planner 建立。若某個模型步驟確實需要看見 reference,也只能使用 value-free alias,不能讓使用者在 prompt 自填 vault、版本或 value。tool proposal 與 policy record 同樣只攜帶 reference;值到真正要呼叫 legacy dependency 前才解析。下面使用 repository 內真的會跑的 evidence API,不把雲端示意碼偽裝成完成品:

from magic_panda_agent.stages.day19 import cumulative_stage_replay

evidence = cumulative_stage_replay()

assert evidence["disabled_before"]["reason_code"] == (
    "LEGACY_DISABLED_SECRET_ACCEPTED"
)
assert evidence["disabled_before"]["details"]["selected_enabled"] is False
assert evidence["disabled_after"]["reason_code"] == "SECRET_VERSION_DISABLED"
assert evidence["disabled_after"]["receipt_count"] == 0
assert evidence["confused_after"]["reason_code"] == (
    "SECRET_PROPOSAL_BINDING_DENIED"
)
assert evidence["current_after"]["reason_code"] == "SECRET_REFERENCE_RESOLVED"
assert evidence["current_after"]["details"]["selected_version"] == 2
assert evidence["current_after"]["details"]["pipeline_stages"] == [
    "request", "proposal", "policy", "adapter"
]
assert evidence["current_after"]["details"]["adapter_binding_verified"] is True
assert evidence["current_after"]["details"]["after_hits"] == 0

底層仍是 LocalRotatingSecretStore;它的 reader key 使用 (tenant_id, subject),不是只有 subject。因此 moon-rabbit-lab 中剛好也叫 vault-agent 的 principal,不能吃到 bamboo-hq 的 grant。

current_after 的公開證據只留下 reference alias、version 與 redaction count,不含 secret value。實際 shape 是:

{
  "resource_id": "vendor-token:v2",
  "reason_code": "SECRET_REFERENCE_RESOLVED",
  "receipt_count": 1,
  "side_effect_count": 0,
  "details": {
    "selected_version": 2,
    "selected_enabled": true,
    "before_hits": 5,
    "after_hits": 0
  }
}

正式使用 Azure SDK 時,同樣不要記錄 get_secret(...).value。以下是尚未在 V2 雲端執行的 adapter pseudocode,不是本機 evidence:

from azure.identity import ManagedIdentityCredential
from azure.keyvault.secrets import SecretClient

credential = ManagedIdentityCredential()
client = SecretClient(
    vault_url="https://<vault-name>.vault.azure.net",
    credential=credential,
)

secret = client.get_secret("vendor-token")
call_legacy_dependency(secret.value)  # value 不進 prompt、log、trace

範例中的 placeholder 必須由部署設定提供,不能接受 request body 傳入任意 vault
URL。正式 adapter 也應限制 cloud、scheme、host、port、userinfo、query 與 path,
避免將 bearer 送往看起來很像 Key Vault 的 hostile endpoint。本篇還多綁一層「核准的
vault resource」:另一個同樣長得像 *.vault.azure.net 的合法 URL 仍得到
KEY_VAULT_RESOURCE_DENIED。URL 長得像 Azure,不代表它就是這筆部署核准的 vault。

Rotation drill 要把 cache 與撤銷排進同一場演習

一輪可驗收的 rotation 不是按下「New Version」就散會:

建立新 credential/secret version
→ 下游暫時接受新舊值(若產品支援)
→ workload 取得新版本
→ 清除 process 與 distributed cache
→ 用新值完成正向 smoke
→ 停用/撤銷舊值
→ 用舊值執行負向 smoke,要求失敗
→ 觀察錯誤率與舊版本使用量
→ 保存 rollback 與事件紀錄

下游若只能同時接受一把 key,就沒有魔法般的 zero-downtime rotation;要安排 maintenance window 或雙端協調。cache 應有短 TTL、版本感知與明確 invalidation,不能只靠 pod 未來某天剛好重啟。

Soft delete 與 purge protection 也不是同一個開關。Microsoft 建議同時啟用 soft delete、purge protection 與自動 rotation;soft delete 提供 7–90 天的 recovery window,purge protection 才能在 retention 到期前阻止永久清除。Secure Key Vault(查閱:2026-08-03)

Foundry BYO Key Vault 是 connection 管理,不是 runtime 讀值

若大魔術熊貓工程司替 Foundry resource 設定 customer-owned Key Vault 來保存 connection secrets,這條權限與 FastAPI runtime 讀單一 vendor secret 不同。官方文件目前列出幾個重要限制:Set up an Azure Key Vault connection(查閱:2026-08-03)

  • 每個 Foundry resource 限一個 Key Vault connection。
  • 建立時 resource/project 不能已有其他 connections;IaC 應先建立 Key Vault connection。
  • 不支援自動 secret migration;更換 vault 要重建 connection secrets。
  • 刪除底層 vault/connection secret 會讓相依 connection 失效。
  • Key Vault Secrets Officer role assignment 最多可能需要約 30 分鐘傳播。

官方範例將 Foundry resource managed identity 指派 Key Vault Secrets Officer,這是管理 connection secrets 的角色;不要照搬給只需讀一個 secret 的 Agent runtime。前者要 set/delete lifecycle,後者通常只需 Secrets User

Test:Version 1 要真的失敗,Version 2 不能留下 canary

畫面判讀目標: 確認 response、event、outbox、error 與 trace 都沒有 secret canary。

五個 sink 比較顯示修補前各命中、修補後合成 canary 命中為零。

Key Vault 管入口,應用仍要管值離開後的每個 sink。 可觀察狀態:五個 sink before_hits=5、after_hits=0,畫面只顯示 alias 與 count。 Claim boundary:canary scan 不涵蓋未知 encodings、heap、crash dump 或 external telemetry。

畫面判讀目標: 把 version selection 與三條 reader-denial 路徑從 sink scan、endpoint policy 分開驗收。

Secret rotation UI 顯示舊版拒絕、current 和 resolved version,以及三條 reader denial。

Rotation 驗收先確認舊版真的失敗,且讀取權限沒有跨 principal、tenant 或 secret name 漂移。 可觀察狀態:old_enabled=false、old_version_result=SECRET_VERSION_DISABLED、current_version=resolved_version=2;unauthorized、cross-tenant same-subject 與 other-name 均為 SECRET_READER_DENIED。 Claim boundary:未呼叫 Azure Key Vault;畫面不宣稱 Receipt、cache invalidation 或雲端 rotation。

畫面判讀目標: 獨立檢視 Key Vault endpoint shape 與 approved-resource alias policy,不讓它被 rotation/sink 欄位淹沒。

Key Vault endpoint policy matrix 顯示合法 alias 通過,八種 hostile shape 與 wrong resource 全部被拒絕。

URL 長得像 Azure endpoint 不代表它就是獲准的 vault resource。 可觀察狀態:valid alias 解析成功;http、suffix、userinfo、port、path、query、fragment、whitespace 均拒絕,wrong resource 得到 KEY_VAULT_RESOURCE_DENIED。 Claim boundary:只測 synthetic URL parser 與 local allowlist;未建立 Azure Key Vault connection,也未驗證 DNS、private endpoint 或 Azure RBAC。

進入 day19/ 後執行:

uv run pytest tests/stages/day19/test_acceptance.py -q

V2 acceptance 必須確認:

  • 未授權 principal、相同 subject/不同 tenant、以及未授權 secret name 都得到 SECRET_READER_DENIED
  • decision provenance、action hash 或 reference 被竄改時,adapter 在 get_version() 前拒絕,spy store read count 維持 0。
  • rotation 後 old version 為 disabled,實際 get_version() 得到 SECRET_VERSION_DISABLED
  • before profile 仍接受 disabled version 並產生 receipt;after profile receipt 為 0。
  • strict SecretReference 拒絕額外 value、path traversal 與型別 coercion;scope 由 server 派生。
  • request/proposal reference 置換與 policy action-hash 竄改都在 adapter 前失敗。
  • Azure-shaped 但非核准 resource 的 vault URL 得到 KEY_VAULT_RESOURCE_DENIED
  • enabled version 2 可由已通過身分與 Key Vault data-role fixture 的 runtime 解析。
  • raw response、event、outbox、error、trace 先命中 canary,export 後每個 sink 都是 0。

本次 Day 19 acceptance 為 5 passed。攻擊/before-state UI 要顯示 disabled version 在 before
被接受、after 被拒絕、D19-A02 reference 置換,以及 endpoint/resource matrix;
不得顯示 secret value。從 day19/ 執行:

PYTHONPATH=src:. uv run python scripts/render_stage_evidence.py --day 19 --evidence-id d19-disabled-secret-version-matrix

修補/test UI 要顯示 request → proposal → policy → adapter、action-hash binding、
version 2、五個 sink 名稱、before_hits=5after_hits=0,只保留 synthetic alias
與 count。JSON blocker 要保留 AZURE_KEY_VAULT_REQUEST_NOT_EXERCISED

PYTHONPATH=src:. uv run python scripts/render_stage_evidence.py --day 19 --evidence-id d19-current-secret-redaction

這些 structured capture 命令只重現 disclosure-safe JSON;正式發布時,本篇 required app UI 圖
必須由目前相關原始碼經 repository UI capture pipeline 產生,schema 3 manifest 必須以 source-tree SHA-256
與檔案數綁定輸入並交由 strict verifier 重算。正式圖仍只支持本機 secret-store/輸出邊界證據;
AZURE_KEY_VAULT_REQUEST_NOT_EXERCISED 仍成立,沒有雲端 secret receipt。

雲端驗證邊界:PENDING-CLOUD

V2 尚未建立 Key Vault、Private Endpoint、runtime MI、BYO Foundry connection,也沒有對真實 secret version 執行 rotation、disable、cache invalidation、soft-delete recovery 或 purge-protection drill。角色傳播與 legacy access-policy self-elevation 也尚未重跑。

所以本篇的 Azure 設定是官方文件核對後的實作規格,本機測試則是 deterministic contract;兩者不能合併寫成 Key Vault 雲端已通過。

Residual Risk:拿得到值的 process,仍可能把值帶走

只要 workload 合法取得 secret,遭入侵的 process 仍可在記憶體中使用或外洩它。Key Vault 不知道某次 get 之後是否被送進模型,也不會自動清除應用 cache。能改用 Managed Identity、workload federation 或短效 token 的 dependency,應優先減少 secret 數量。

本機 rotate() 也沒有跨 thread/process 的 lock、ETag 或 transaction。兩個 operator 同時輪替可能產生競態;正式流程需要 single-writer/lease 或外部協調,並在 rotation 後重新讀取 active version 驗證。

ISO/IEC 27001:2022 與 27002:2022 在這裡只提供 credential 建立、保管、授權、輪替、撤銷與紀錄覆核的治理視角。這份 Lab 沒有 ISMS scope、適用性聲明或稽核,不能寫成符合 ISO。

明天會把 Key Vault 放回網路拓撲:誰能讀值之外,還要確認 DNS 解析到哪裡、PNA 是否關閉,以及 Agent 出站能不能偷偷走到未核准 endpoint。

第一方參考資料


上一篇
Day 18|Microsoft Foundry 的 AI Agent 攻防實戰:Azure Owner 不是宇宙萬能管理員,Foundry RBAC 到底管哪一層?
下一篇
Day 20|Microsoft Foundry 的 AI Agent 攻防實戰:Private Endpoint 也有風險
系列文
Microsoft Foundry 的 AI Agent 攻防實戰:建立高資安與可治理的 Agent 系統28
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言